package hypeerweb.node;


import hypeerweb.SimplifiedNodeDomain;
import hypeerweb.visitor.Parameters;
import hypeerweb.visitor.Visitor;

import java.util.HashSet;
import java.util.Set;

import hypeerweb.HyPeerWeb;

/**
 * A node in the HyPeerWeb. <br>
 * <br>
 * 
 * <pre>
 * <b>Domain:</b>
 *      webId			: WebId
 *      state			: NodeState
 *      connections		: Connections
 * </pre>
 * 
 * @author Ben Romney
 */

public class Node implements Comparable<Node>, NodeInterface {

	private NodeState state;
	private Connections connections;
	private Contents contents;

	/**
	 * The one and only empty node.
	 */
	public static final Node NULL_NODE = new NullNode();

	/**
	 * The default constructor.
	 */
	protected Node() {
		state = null;
		connections = new Connections();
	}

	/**
	 * The constructor for a node given only the new id.
	 * 
	 * @param id
	 *            - The ID for the new node.
	 * @pre None.
	 * @post None.
	 */
	public Node(final int id, final int height) {
		connections = new Connections(new WebId(id, height));
		setFold(this);
	}
	public Node(final int id) {
		connections = new Connections(new WebId(id));
		setFold(this);
	}

	@Override
	public SimplifiedNodeDomain constructSimplifiedNodeDomain() {
		SimplifiedNodeDomain snd = new SimplifiedNodeDomain(getWebId()
				.getValue(), getWebId().getHeight(), createSetOfNeighborIds(),
				createSetOfUpPointerIds(), createSetOfDownPointerIds(),
				getFold().getIdInt(), getSurrogateFold().getIdInt(),
				getInverseSurrogateFold().getIdInt(), state);
		return snd;
	}

	@Override
	public void addToHyPeerWeb(Node newNode) {
		Node terminalNode = findTerminalNode();
		Node insertionNode = terminalNode.findInsertionPoint(terminalNode);
		insertionNode.makeChild(newNode);
		HyPeerWeb.getSingleton().addNode((Node) newNode);
	}

	/**
	 * Sets up the new child node's connections, then notifies the its neighbors
	 * to update their own connections.
	 * 
	 * @param child
	 *            The new node being added to the HyPeerWeb
	 * @pre The method is being called on the insertion point node.
	 * @post The child's connections will be properly established.
	 */
	private void makeChild(Node child) {
		child.setConnections(new Connections(this.getWebId().createChildsWebId()));
		child.setDownPointers(getBiggerNeighbors());
		child.state = NodeState.TERMINAL;
		child.addNeighbor(this);
		getWebId().incrementHeight();
		for (Node neighbor : getUpPointers()) {
			child.addNeighbor(neighbor);
		}
		calculateFoldsAddNode(child);
		notifyNeighbors(child);
		this.connections.updateEveryoneChildList();
	}

	@Override
	public void removeFromHyPeerWeb() {
		Node terminalNode = findTerminalNode();
		Node replacementNode = terminalNode.findDeletionPoint(terminalNode);
		HyPeerWeb.getSingleton().removeNode(replacementNode);
		HyPeerWeb.getSingleton().removeNode(this);
		removeChild(replacementNode);
	}

	/**
	 * Establishes new connections after node deletion.
	 * 
	 * @pre The method is called on the node to be deleted. The node being
	 *      passed in is the last node of the HyPeerWeb.
	 * @post Proper connections are established.
	 */
	private void removeChild(Node replacementNode) {
		Node parentOfReplacement = replacementNode
				.getSmallestNeighborWithChild();
		parentOfReplacement.getWebId().decrementHeight();

		replacementNode.connections.removeMe(replacementNode);

		
		replacementNode.calculateFoldsRemoveNode();

		for (Node neighbor : replacementNode.getNeighbors()) {
			if (neighbor != parentOfReplacement) {
				neighbor.addDownPointer(parentOfReplacement);
				parentOfReplacement.addUpPointer(neighbor);
			} 
		}
		parentOfReplacement.connections.updateEveryoneChildList();
		if (replacementNode != this) {
			replacementNode.setConnections(connections);
			replacementNode.state = state;
			replacementNode.connections.removeMe(this);
			replacementNode.connections.addMe(replacementNode);
			replacementNode.setFold(replacementNode.getFold());
			HyPeerWeb.getSingleton().addNode(replacementNode);
		}
	}

	/**
	 * Uses a recursive greedy algorithm to find the estimated terminal node.
	 * 
	 * @return The estimated terminal node.
	 * @pre Nodes in the HyPeerWeb have the proper states assigned to them.
	 * @post The Node returned is not necessarily the insertion point.
	 */
	private Node findTerminalNode() {
		if (state == NodeState.TERMINAL || state == NodeState.HYPEERCUBECAP) {
			return this;
		}
		return getBiggestNeighbor().findTerminalNode();
	}

	/**
	 * A recursive squeeze algorithm to find the insertion point for adding a
	 * node.
	 * 
	 * @param lowerBound
	 *            The lower bound of the possibility range.
	 * @param upperBound
	 *            The upper bound of the possibility range.
	 * @return The Node to be used as the insertion point.
	 * @pre Nodes in the HyPeerWeb have the proper states assigned to them.
	 * @post The Node returned is the insertion point.
	 */
	private Node findInsertionPoint(Node lowerBound) {
		return new FindNode(this) {
			@Override
			public Node getNode(Node lowerBound, Node upperBound) {
				return upperBound;
			}

			@Override
			public Node getHyPeerCubeCapCase(Node node) {
				return root.getFold();
			}
		}.execute(lowerBound, NULL_NODE);
	}

	/**
	 * Executes the squeeze algorithm to find the last node in the web.
	 * 
	 * @return The last node in the web.
	 * @pre lowerBound is the node found findTerminalNode().
	 * @post Returns the last node in the web.
	 */
	private Node findDeletionPoint(Node lowerBound) {
		return new FindNode(this) {
			@Override
			public Node getNode(Node lowerBound, Node upperBound) {
				return lowerBound;
			}

			@Override
			public Node getHyPeerCubeCapCase(Node node) {
				return root;
			}
		}.execute(lowerBound, NULL_NODE);
	}

	/**
	 * Checks to see if we are done finding the insertion point.
	 * 
	 * @param lowerBound
	 *            The lower bound of the possibility range.
	 * @param upperBound
	 *            The upper bound of the possibility range.
	 * @return True if upperBound is the insertion point, false otherwise.
	 * @pre None.
	 * @post Will not change any nodes or connections.
	 */
	private boolean isFindPoint(Node lowerBound, Node upperBound) {
		return lowerBound.getWebId().getValue() + 1 == upperBound.getWebId()
				.createChildsWebId().getValue();
	}

	/**
	 * The class that findInsertionPoint() and findDeletionPoint() delegate
	 * their algorithm to.
	 */
	private abstract class FindNode {
		protected Node root;

		public FindNode(Node root) {
			this.root = root;
		}

		public Node execute(Node lowerBound, Node upperBound) {
			if (isFindPoint(lowerBound, upperBound)) {
				return getNode(lowerBound, upperBound);
			}
			switch (root.state) {
			case HYPEERCUBECAP:
				return getHyPeerCubeCapCase(Node.this);
			case DOWNPOINTING:
			case TERMINAL:
				upperBound = lowerBound.getSmallestSurrogateNeighbor();
				root = upperBound;
				return execute(lowerBound, upperBound);
			case UPPOINTING:
				Node bisn = upperBound.getBiggestInverseSurrogateNeighbor();
				if (bisn.getIdInt() > lowerBound.getIdInt()) {
					lowerBound = bisn;
					root = lowerBound;
					return execute(lowerBound, upperBound);
				} else {
					upperBound = upperBound.getSmallestNeighborWithoutChild();
					root = upperBound;
					return execute(lowerBound, upperBound);
				}
			default:
				throw new ArrayIndexOutOfBoundsException(
						"\nNot a valid state: " + state);
			}
		};

		public abstract Node getNode(Node upperBound, Node lowerBound);

		public abstract Node getHyPeerCubeCapCase(Node node);
	};

	/**
	 * Updates the fold, surrogate fold, and inverse surrogate fold for the
	 * child, parent, and the parent's fold. Used when adding a node.
	 * 
	 * @param c
	 *            The child node.
	 */
	private void calculateFoldsAddNode(Node c) {
		if (state == NodeState.HYPEERCUBECAP) {
			c.setFold(this);
			this.setFold(c);
		} else if (this.getInverseSurrogateFold() == NULL_NODE) {
			Node childX = c;
			Node parentX = this;
			Node parentY = this.getFold();
			childX.setFold(parentY);
			parentX.setFold(NULL_NODE);
			parentX.setSurrogateFold(parentY);
			parentY.setFold(childX);
			parentY.setInverseSurrogateFold(parentX);
		} else {
			Node childY = c;
			Node parentY = this;
			Node parentX = parentY.getInverseSurrogateFold();
			childY.setFold(parentX);
			parentY.setInverseSurrogateFold(NULL_NODE);
			parentX.setFold(childY);
			parentX.setSurrogateFold(NULL_NODE);
		}
	}

	/**
	 * Updates the folds, surrogate folds, and inverse surrogate folds. Used
	 * when deleting a node.
	 * 
	 * @pre Method is called on the deletion node?
	 * @post All requisite fold adjustments are made.
	 */
	private void calculateFoldsRemoveNode() {
		if (getWebId().toString().equals("1")) {
			getFold().setFold(getFold());
		} else if (this.getFold().getWebId().getHeight() == this.getWebId()
				.getHeight()) {
			Node parentX = this.getFold();
			parentX.setSurrogateFold(this.getSmallestNeighborWithChild());
			getSmallestNeighborWithChild().setInverseSurrogateFold(parentX);

		} else {
			Node parentY = getFold();
			Node smallest = getSmallestNeighborWithChild();
			smallest.setFold(parentY);
			smallest.setInverseSurrogateFold(NULL_NODE);
			smallest.setSurrogateFold(NULL_NODE);
			parentY.setFold(smallest);
			parentY.setInverseSurrogateFold(NULL_NODE);
			parentY.setSurrogateFold(NULL_NODE);
		}
	}

	/**
	 * Updates the connections of the new node's neighbors to include the new
	 * node.
	 * 
	 * @param node
	 *            The new node.
	 */
	private void notifyNeighbors(Node node) {
		node.connections.addMe(node);
		for (Node downPointer : node.getDownPointers()) {
			downPointer.addUpPointer(node);
		}
		for (Node neighbor : node.getNeighbors()) {
			this.removeUpPointer(neighbor);
			neighbor.addNeighbor(node);
			neighbor.removeDownPointer(this);
		}
		for (Node parentNeighbors : getNeighbors()) {
			parentNeighbors.removeNeighbor(this);
			parentNeighbors.addNeighbor(this);
		}
	}


	private Node getFold() {
		return connections.getFold();
	}

	private Node getSurrogateFold() {
		return connections.getSurrogateFold();
	}

	private Node getInverseSurrogateFold() {
		return connections.getInverseSurrogateFold();
	}

	private Node getSmallestSurrogateNeighbor() {
		return connections.getSmallestSurrogateNeighbor();
	}

	private Node getBiggestInverseSurrogateNeighbor() {
		return connections.getBiggestInverseSurrogateNeighbor();
	}

	
	private Node getSmallestNeighborWithoutChild() {
		return connections.getSmallestNeighborWithoutChild();
	}

	public Node getSmallestNeighborWithChild() {
		return connections.getSmallestNeighborWithChild();
	}

	private Node getBiggestNeighbor() {
		return connections.getBiggestNeighbor();
	}

	private Set<Node> getBiggerNeighbors() {
		return connections.getBiggerNeighbors();
	}

	private Set<Node> getDownPointers() {
		return connections.getDownPointers();
	}

	private Set<Node> getUpPointers() {
		return connections.getUpPointers();
	}

	private Set<Node> getNeighbors() {
		return connections.getNeighbors();
	}

	private void setDownPointers(Set<Node> downPointers) {
		connections.setDownPointers(downPointers);
	}

	@Override
	public void addNeighbor(Node neighbor) {
		connections.addNeighbor(neighbor);
		if (!connections.getBiggerNeighbors().isEmpty()
				&& state == NodeState.TERMINAL) {
			state = NodeState.DOWNPOINTING;
		}
	}

	@Override
	public void removeNeighbor(Node neighbor) {
		connections.removeNeighbor(neighbor);
	}

	@Override
	public void addUpPointer(Node upPointer) {
		connections.addUpPointer(upPointer);
		state = NodeState.UPPOINTING;
	}

	@Override
	public void removeUpPointer(Node upPointer) {
		connections.removeUpPointer(upPointer);
		if (connections.getUpPointers().isEmpty()) {
			state = NodeState.STANDARD;
		}
	}

	@Override
	public void addDownPointer(Node downPointer) {
		connections.addDownPointer(downPointer);

		if (getBiggerNeighbors().isEmpty()) {
			state = NodeState.TERMINAL;
		} else {
			state = NodeState.DOWNPOINTING;
		}
	}

	@Override
	public void removeDownPointer(Node downPointer) {
		connections.removeDownPointer(downPointer);
		if (connections.getDownPointers().isEmpty()
				&& state == NodeState.DOWNPOINTING) {
			state = NodeState.STANDARD;
		}
	}

	@Override
	public void setFold(Node newFold) {
		if (newFold.getWebId().getValue() == 0) {
			state = NodeState.HYPEERCUBECAP;
		} else if (state == NodeState.HYPEERCUBECAP) {
			state = NodeState.STANDARD;
		}
		connections.setFold(newFold);
	}

	@Override
	public void setSurrogateFold(Node newSurrogateFold) {
		connections.setSurrogateFold(newSurrogateFold);
	}

	@Override
	public void setInverseSurrogateFold(Node newInverseSurrogateFold) {
		connections.setInverseSurrogateFold(newInverseSurrogateFold);
	}

	/**
	 * Creates a set of integers for neighbors to be used for the simplified
	 * node domain.
	 * 
	 * @return Returns a set of integers corresponding to the neighbors of this
	 *         node
	 */
	private HashSet<Integer> createSetOfNeighborIds() {
		HashSet<Integer> neighborIds = new HashSet<Integer>();
		for (Node n : connections.getNeighbors()) {
			neighborIds.add(n.getIdInt());
		}
		return neighborIds;
	}

	/**
	 * Creates a set of integers for upPointers to be used for the simplified
	 * node domain.
	 * 
	 * @return Returns a set of integers corresponding to the upPointers of this
	 *         node
	 */
	private HashSet<Integer> createSetOfUpPointerIds() {
		HashSet<Integer> upPointerIds = new HashSet<Integer>();
		for (Node n : connections.getUpPointers()) {
			upPointerIds.add(n.getIdInt());
		}
		return upPointerIds;
	}

	/**
	 * Creates a set of integers for downPointers to be used for the simplified
	 * node domain.
	 * 
	 * @return Returns a set of integers corresponding to the downPointers of
	 *         this node
	 */
	private HashSet<Integer> createSetOfDownPointerIds() {
		HashSet<Integer> downPointerIds = new HashSet<Integer>();
		for (Node n : connections.getDownPointers()) {
			downPointerIds.add(n.getIdInt());
		}
		return downPointerIds;
	}

	boolean hasChild() {
		return state == NodeState.STANDARD;
	}

	/**
	 * Gets the node's webId binary value as an integer.
	 * 
	 * @return Returns the node's webId binary value.
	 */
	private int getIdInt() {
		WebId myWebId = connections.getWebId();
		return (myWebId == null) ? -1 : myWebId.getValue();
	}

	@Override
	public WebId getWebId() {
		return connections.getWebId();
	}
	
	@Override
	public void setWebId(WebId id) {
		connections.setWebId(id);
	}

	@Override
	public int compareTo(Node o) {
		return this.getIdInt() - o.getIdInt();
	}

	@Override
	public String toString() {
		return connections.getWebId().toString();
	}

	@Override
	public NodeState getState() {
		return state;
	}

	@Override
	public void setState(NodeState state) {
		this.state = state;
	}

	@Override
	public Node asNode() {
		return this;
	}

	@Override
	public Contents getContents() {
		if (contents == null) {
			contents = new Contents();
		}
		return contents;
	}
	
	@Override
	public void accept(Visitor visitor, Parameters parameters) {
		visitor.visit(this, parameters);
	}

	public Node getClosestNodeTo(int target) {
		return connections.getClosestNodeTo(target, this);
	}
	
	public void updateChildList(){
		connections.updateChildList();
	}
	protected void setConnections(Connections c) {
		this.connections = c;
	}
	public NodeInterface getNodeByWebId(int id) {
		return connections.getNodeByWebId(id);
	}
}